/**
 *
 */
package com.honeybees.framework.sso.interceptor;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import com.alibaba.fastjson.JSON;
import com.honeybees.framework.common.util.Encoder;
import com.honeybees.framework.common.util.HTTPUtils;
import com.honeybees.framework.common.util.Utils;
import com.honeybees.framework.sso.SessionConsts;
import com.honeybees.framework.sso.session.MockSession;
import com.honeybees.framework.sso.session.manager.MockSessionManager;

/**
 * <dl>
 * <dt><b> 单点登录验证 拦截器 </b></dt>
 * <p>
 * <dd>http://labreeze.iteye.com/blog/2213654 <br>
 * http://www.cnblogs.com/davidwang456/p/4108355.html</dd>
 * </dl>
 * <p>
 * Copyright (C) All rights reserved.
 * </p>
 *
 * @author 李远明
 * @version 2016-09-20 11:12:41 根据 SSOFilter 改造 SSOInterceptor，不再使用 SSOFilter：
 *          <p>
 *          <b> HandlerInterceptor 一般用于权限验证，以及一些处理风格本地化等公共代码。 <br>
 *          Filter 一般用于修改请求内容和界面的解析处理相关。</b>
 *          <p>
 *          而且 SSOFilter 的配置更复杂，需要在 Spring 中配置其属性，还要在 web.xml 中配置代理：DelegatingFilterProxy
 *          <p>
 *          Filter 会在 HandlerInterceptor 前面处理
 */
public final class SSOInterceptor extends HandlerInterceptorAdapter {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 登录路径
     *
     * @version 2017-03-16 15:47:43 ssoLoginUri => myLoginUrl
     */
    private String myLoginUrl;
    /**
     * 管理系统上下文
     */
    private String mySystemContext;
    /**
     * 不过滤的路径
     */
    private List<String> excludeUriPatterns;

    /**
     * Session缓存管理
     */
    @Resource
    private MockSessionManager mockSessionManager;

    /**
     * 该方法将在请求处理之前进行调用
     * <p>
     * 与Session相关的三个变量：KEY_COOKIE_seed、KEY_COOKIE_sessionId、KEY_COOKIE_userId，在登录与本方法中设值。
     * </p>
     *
     * @author 李远明
     * @version 2016-09-20 11:17:06 根据 SSOFilter.doFilter(..) 方法改造
     * @version 2017-03-01 22:19:16 优化代码：复用 request.getAttribute(KEY_SESSION_IN_REQUEST)
     * @version 2017-05-11 08:50 不再拼接前一部分 loginUrl，改为全部在 doRedirect 方法中实现
     * @version 2017-07-03 09:53 修改 expected 为 desired；Red Hat Enterprise Linux Installation："Desired Capacity"
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");

        String contextPath = request.getContextPath();
        // String url = request.getRequestURL().toString(); // "http://localhost:8080/sysmgmt/login"
        String uri = request.getRequestURI(); // "/sysmgmt/login"

        String method = request.getMethod();
        this.logger.debug("==> " + method + ": " + uri);

        if (method != null && "GET".equals(method)) {
            String queryString = HTTPUtils.getRequestParameter(request);
            this.logger.debug("==> QueryString: " + queryString);
        }

        String seed = null; // 编码随机数
        String sessionId = null;
        String encodedUserId = null; // user.id, not userNo

        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                String name = cookie.getName();
                String value = cookie.getValue();
                if (SessionConsts.KEY_COOKIE_seed.equals(name)) {
                    seed = value;
                } else if (SessionConsts.KEY_COOKIE_sessionId.equals(name)) {
                    sessionId = value;
                } else if (SessionConsts.KEY_COOKIE_userId.equals(name)) {
                    encodedUserId = value;
                }
            }
        }

        String browserAgent = HTTPUtils.getBrowserAgent(request);
        if (StringUtils.isBlank(seed) || StringUtils.isBlank(sessionId)) { // 初次请求
            // 请求中没有 Session 信息，生成，并设置到 Cookie 中
            seed = Utils.shortUUID(); // 自定义的 UUID 2016-07-25 14:55
            sessionId = Encoder.encode(seed, browserAgent, SessionConsts.KEY_COOKIE_ENCRYPT_KEY);
            HTTPUtils.setHttpOnlyCookie(response, SessionConsts.KEY_COOKIE_seed, seed, contextPath, null, -1, false);
            HTTPUtils.setHttpOnlyCookie(response, SessionConsts.KEY_COOKIE_sessionId, sessionId, contextPath, null, -1,
                    false);
        } else { // 有 seed 与 sessionId 信息，校验
            String desired_sessionId = Encoder.encode(seed, browserAgent, SessionConsts.KEY_COOKIE_ENCRYPT_KEY);
            if (!desired_sessionId.equals(sessionId)) { // 更换浏览器版本……
                this.logger.error("Cookie信息不符，期望 sessionId=" + desired_sessionId);
                this.clearCookie(request, response);
                this.logFakeRequest(request, browserAgent, seed, sessionId, encodedUserId);
                this.doRedirect(request, response, uri); // uri 2017-03-04 00:09:30
                return false;
            }
            if (!StringUtils.isBlank(encodedUserId)) { // Session中有用户信息（已登录），则验证是否正确（可能是伪造）
                MockSession session = this.mockSessionManager.getMockSession(sessionId);
                if (session == null) { // 缓存被删除（del），或超时
                    // this.logger.error("Session超时，即将跳转到登录页面。");
                    this.clearCookie(request, response);
                    this.doRedirect(request, response, uri);
                    return false;
                } else {
                    String userId = session.getUserId();
                    String desired_userId = Encoder.encode(userId, sessionId, SessionConsts.KEY_COOKIE_ENCRYPT_KEY);
                    if (!desired_userId.equals(encodedUserId)) {
                        this.logger.error("Cookie信息不符，期望 userId=" + desired_userId);
                        this.clearCookie(request, response);
                        this.logFakeRequest(request, browserAgent, seed, sessionId, encodedUserId);
                        this.doRedirect(request, response, uri);
                        return false;
                    } else { // 正常
                        // 用户登录成功，且 Session 未超时 2016-08-19 16:50
                        request.setAttribute(SessionConsts.KEY_SESSION_IN_REQUEST, session);
                        // request.setAttribute(KEY_SESSION_USER_ID_IN_REQUEST, userId);
                        this.mockSessionManager.updateSessionIdByUserId(userId, sessionId); // 刷新userId对应的sessionId
                    }
                }
            }
        }
        // 没有登录，一些公共请求（如 public/captcha）也能使用sessionId 2017-03-09 12:33:42
        request.setAttribute(SessionConsts.KEY_SESSION_ID_IN_REQUEST, sessionId);
        // SessionUtils.setSession(session); // 方便获取Session 2016-05-27 13:12 李远明
        // SessionUtils.setSessionId(sessionId); // 方便获取SessionID 2016-05-27 13:12 李远明

        if (this.isSkipUri(contextPath, uri)) {
            request.setAttribute(SessionConsts.KEY_SKIP_URI_IN_REQUEST, true);
            return true;
        } else {
            Object reqSession = request.getAttribute(SessionConsts.KEY_SESSION_IN_REQUEST);
            if (reqSession == null) { // 缓存被清除（logout）
                this.logger.error("当前请求中的Session为NULL，即将跳转到登录页面。");
                this.doRedirect(request, response, uri);
            } else {
                MockSession session = (MockSession) reqSession;
                String rejectMsg = session.getRejectMsg();
                if (StringUtils.isBlank(rejectMsg)) { // 登录状态正常
                    // String userId = session.getUserId();
                    // request.setAttribute(KEY_SESSION_USER_ID_IN_REQUEST, userId);
                    return true;
                } else { // 用户ID在其他机器或浏览器上登录，当前Session已失效
                    this.mockSessionManager.removeSession(session.getId());
                    this.clearCookie(request, response);
                    this.doRedirect(request, response, uri);
                }
            }
        }
        return true;
    }

    /**
     * 清理Cookie
     *
     * @param request  当前请求
     * @param response 当前响应
     * @author 李远明
     * @version 2016-09-20 11:22:02
     */
    private void clearCookie(HttpServletRequest request, HttpServletResponse response) {
        String contextPath = request.getContextPath();
        HTTPUtils.setHttpOnlyCookie(response, SessionConsts.KEY_COOKIE_seed, "", contextPath, null, 0, false);
        HTTPUtils.setHttpOnlyCookie(response, SessionConsts.KEY_COOKIE_sessionId, "", contextPath, null, 0, false);
        HTTPUtils.setHttpOnlyCookie(response, SessionConsts.KEY_COOKIE_userId, "", contextPath, null, 0, false);
    }

    /**
     * 日志记录伪造的请求
     *
     * @param request       请求
     * @param browserAgent  浏览器代理
     * @param seed          seed
     * @param sessionId     sessionId
     * @param encodedUserId 用户编号
     * @author 李远明
     * @version 2016-09-20 11:22:58
     */
    private void logFakeRequest(HttpServletRequest request, String browserAgent, String seed, String sessionId,
                                String encodedUserId) {
        this.logger.error("--------------------客户端伪造了cookie-------------------URI=" + request.getRequestURI());
        this.logger.error("客户端IP：" + HTTPUtils.getRealIP(request));
        this.logger.error("浏览器标头：" + browserAgent);
        this.logger.error("请求URI（POST方法，见参数的 _method）：" + request.getMethod() + " @ " + request.getRequestURI());
        this.logger.error("cookie（seed）：" + seed);
        this.logger.error("cookie（sessionId）：" + sessionId);
        this.logger.error("cookie（encodedUserId）：" + encodedUserId);

        String parameter = HTTPUtils.getRequestParameter(request);
        this.logger.error("请求参数：" + parameter);
    }

    /**
     * <dl>
     * <dt><b> 是否跳过拦截 </b></dt>
     * <p>
     * <dd>功能描述</dd>
     * </dl>
     *
     * @param contextPath 项目根路径
     * @param uri         当前路径
     * @return true-跳过，false-不跳过
     * @author 李远明
     * @version 2016-09-20 11:29:23
     */
    private boolean isSkipUri(String contextPath, String uri) {
        if (uri.endsWith(".js") || uri.endsWith(".css") || uri.endsWith(".png") || this.isIndex(contextPath, uri)) {
            return true;
        } else {
            List<String> patterns = this.excludeUriPatterns;
            for (String pattern : patterns) {
                String skipPath = contextPath + pattern;
                // return uri.startsWith(skipPath);
                if (uri.startsWith(skipPath)) {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * <dl>
     * <dt><b> 标题 </b></dt>
     * <p>
     * <dd>功能描述</dd>
     * </dl>
     *
     * @param contextPath 项目根路径
     * @param uri         当前路径
     * @return true/false
     * @author 李远明
     * @version 2016-09-20 11:32:09
     */
    private boolean isIndex(String contextPath, String uri) {
        return uri.equals(contextPath) || uri.equals(contextPath + "/") || uri.equals(contextPath + "/index")
                || uri.equals(contextPath + "/index/");
    }

    /**
     * <dl>
     * <dt><b> 重定向到登录页面 </b></dt>
     * <p>
     * <dd></dd>
     * </dl>
     *
     * @param request  请求
     * @param response 响应
     * @param destUrl  登录成功后，跳转的地址（url/uri）
     * @throws IOException 可能发生的异常
     * @author 李远明
     * @version 2016-09-20 11:30:31
     * @version 2017-03-03 22:29:53 参数loginUrl中不再包含 destUrl 部分，在该方法中添加
     * @version 2017-05-11 08:51 移除参数：loginUrl
     */
    private void doRedirect(HttpServletRequest request, HttpServletResponse response, String destUrl)
            throws IOException {
        if (destUrl.contains("/service/")) {
            String msg = "不允许访问该地址：" + destUrl;
            this.returnFailMsg(request, response, msg);
        } else {
            String method = request.getMethod();
            if ("GET".equals(method)) { // 只有GET请求，登录之后，才能返回原地址 2017-03-03 23:23:54
                // 如果是AJAX请求，即使重定向到登录页面，也不能跳转（暂时没想到办法），所以只返回失败信息 2017-03-05 15:12:50
                boolean ajaxRequest = HTTPUtils.isAjaxRequest(request);
                if (ajaxRequest) {
                    String msg = "当前Session已失效，请重新登录";
                    this.returnFailMsg(request, response, msg);
                } else { // 直接在浏览器地址拦中输入 URL
                    String loginUrl = this.myLoginUrl + "?type=sso&system=" + this.mySystemContext;

                    String queryString = HTTPUtils.getRequestParameter(request);
                    destUrl = destUrl + '?' + queryString; // 拼接查询参数 2017-03-03 22:36:49
                    String encodedDestUrl = URLEncoder.encode(destUrl, "UTF-8");

                    loginUrl = loginUrl + "&destUrl=" + encodedDestUrl;
                    this.logger.debug("请重新登录：" + loginUrl);
                    response.sendRedirect(loginUrl);
                }
            } else if ("POST".equals(method)) {
                // FIXME POST请求一般用ajax发送实现，此时应该使整个页面重新登录，而不是单个请求 2017-03-04 00:23:33
                String msg = "当前Session已失效，请重新登录";
                this.returnFailMsg(request, response, msg);
            } else {
                String msg = "暂不支持该方法：" + method; // 2017-03-05 15:13:28
                this.returnFailMsg(request, response, msg);
            }
        }
    }

    /**
     * 向前端返回失败信息
     *
     * @param request  请求
     * @param response 响应
     * @param msg      信息内容
     * @throws IOException 可能发生的异常
     * @author 李远明
     * @version 2016-09-20 11:33:50
     * @version 2017-03-04 11:42:08 response.reset();
     * @version 2017-05-10 18:05 HTTP 401 错误 - 未授权： (Unauthorized)
     */
    private void returnFailMsg(HttpServletRequest request, HttpServletResponse response, String msg)
            throws IOException {
        response.setContentType("text/html; charset=utf-8");
        response.sendError(401, "session timeout."); // 错误 401.1 - 未经授权：访问由于凭据无效被拒绝。2017-05-10 18:04:27
        Map<String, String> map = new HashMap<String, String>();
        map.put("code", "999999");
        map.put("message", msg);
        PrintWriter out = response.getWriter();
        response.reset(); // getOutputStream getWriter() has already been called for this response
        out.write(JSON.toJSONString(map));
    }

    /**
     * 登录路径
     */
    public void setMyLoginUrl(String myLoginUrl) {
        this.myLoginUrl = myLoginUrl;
    }

    /**
     * 管理系统上下文
     */
    public void setMySystemContext(String mySystemContext) {
        this.mySystemContext = mySystemContext;
    }

    /**
     * 不过滤的路径
     */
    public void setExcludeUriPatterns(List<String> excludeUriPatterns) {
        this.excludeUriPatterns = excludeUriPatterns;
    }

}
